/**
 * Copyright (c) 2013-2019 Nikita Koksharov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.redisson;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;

import org.redisson.api.RFuture;
import org.redisson.api.RLock;
import org.redisson.client.codec.LongCodec;
import org.redisson.client.codec.StringCodec;
import org.redisson.client.protocol.RedisCommands;
import org.redisson.client.protocol.RedisStrictCommand;
import org.redisson.command.CommandAsyncExecutor;
import org.redisson.pubsub.LockPubSub;

/**
 * redisson写锁。写锁只会被一个人持有
 *
 * Lock will be removed automatically if client disconnects.
 *
 * @author Nikita Koksharov
 *
 */
public class RedissonWriteLock extends RedissonLock implements RLock {

    protected RedissonWriteLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
    }

    /**
     * 订阅通道名称: redisson_rwlock:lockName。
     * 当写锁被读锁和写锁阻塞时订阅
     *
     * @return
     */
    @Override
    String getChannelName() {
        return prefixName("redisson_rwlock", getName());
    }

    /**
     * 锁的名称 : ConnectionId:ThreadId:write
     * @param threadId
     * @return
     */
    @Override
    protected String getLockName(long threadId) {
        return super.getLockName(threadId) + ":write";
    }

    /**
     * 覆写的读锁加锁程序
     * @param leaseTime 锁的过期时间。要么是用户指定的过期时间，要么是看门狗设置的过期时间
     * @param unit      时间单位
     * @param threadId  当前线程Id
     * @param command   EVAL_LONG:将LUA脚本返回的值原封返回。EVAL_NULL_BOOLEAN:LUA脚本返回null则转换为返回true，LUA脚本返回非null则转换为返回false
     * @param <T>
     * @return
     */
    @Override
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        // KEYS[1] = lockName
        // ARGV[1] = 过期时间毫秒
        // ARGV[2] = ConnectionId:ThreadId
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                // 锁不存在直接上写锁
                "if (mode == false) then " +
                    "redis.call('hset', KEYS[1], 'mode', 'write'); " +
                    "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return nil; " +
                "end; " +
                // 允许重入自身的写锁
                "if (mode == 'write') then " +
                    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        // 过期时间修改为剩余的ttl+新的ttl
                        "local currentExpire = redis.call('pttl', KEYS[1]); " +
                        "redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
                        "return nil; " +
                    "end; " +
                "end;" +
                // 否则要么存在读锁，要么写锁被他人持有
                "return redis.call('pttl', KEYS[1]);",
            Arrays.<Object>asList(getName()),
            internalLockLeaseTime, getLockName(threadId));
    }

    /**
     * 写锁的解锁程序
     * @param threadId ThreadId
     * @return
     */
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                // 锁不存在。直接向订阅发送'读已解锁'消息
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +

                "if (mode == 'write') then " +
                    "local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
                    // 已不持有锁则返回nil
                    "if (lockExists == 0) then " +
                        "return nil;" +
                    "else " +
                        // 否则重入数自减
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        "if (counter > 0) then " +
                            // 依旧持有重入，更新ttl
                            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                            "return 0; " +
                        "else " +
                            // 完全退出。从lockName中删除自身记录
                            "redis.call('hdel', KEYS[1], ARGV[3]); " +
                            // 如果lockName中只剩下了一条mode记录，则删除整个锁本身，并向订阅发送'读已释放'
                            "if (redis.call('hlen', KEYS[1]) == 1) then " +
                                "redis.call('del', KEYS[1]); " +
                                "redis.call('publish', KEYS[2], ARGV[1]); " +
                            // 否则就是持有写锁的线程重入了读锁，则保留重入的读锁，将读写锁改为读锁模式
                            "else " +
                                "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                            "end; " +
                            "return 1; "+
                        "end; " +
                    "end; " +
                "end; " +
                "return nil;",
        Arrays.<Object>asList(getName(), getChannelName()), 
        LockPubSub.READ_UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }
    
    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }

    @Override
    public RFuture<Boolean> forceUnlockAsync() {
        cancelExpirationRenewal(null);
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
              "if (redis.call('hget', KEYS[1], 'mode') == 'write') then " +
                  "redis.call('del', KEYS[1]); " +
                  "redis.call('publish', KEYS[2], ARGV[1]); " +
                  "return 1; " +
              "end; " +
              "return 0; ",
              Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.READ_UNLOCK_MESSAGE);
    }

    @Override
    public boolean isLocked() {
        RFuture<String> future = commandExecutor.writeAsync(getName(), StringCodec.INSTANCE, RedisCommands.HGET, getName(), "mode");
        String res = get(future);
        return "write".equals(res);
    }

}
